Skip to content

Wire eth_subscribe(newHeads) for Autobahn (CON-257)#3419

Open
wen-coding wants to merge 8 commits into
mainfrom
wen/add_eth_subscribe_newhead_for_autobahn
Open

Wire eth_subscribe(newHeads) for Autobahn (CON-257)#3419
wen-coding wants to merge 8 commits into
mainfrom
wen/add_eth_subscribe_newhead_for_autobahn

Conversation

@wen-coding
Copy link
Copy Markdown
Contributor

@wen-coding wen-coding commented May 11, 2026

Summary

  • Under Autobahn, eth_subscribe("newHeads") is currently broken: the legacy fan-out subscribes to the Tendermint event bus on tm.event = 'NewBlockHeader', but Autobahn drives app.FinalizeBlock from giga_router rather than CometBFT consensus, so PublishEventNewBlockHeader never fires.
  • This PR adds a direct in-process notifier from giga_router's post-commit point into evmrpc.SubscriptionAPI's fan-out goroutine, scoped strictly to Autobahn mode (notifier is only constructed when tmConfig.AutobahnConfigFile != ""). The legacy CometBFT path is untouched.
  • Notifier semantics: single-producer, single-consumer, capacity 1, overwrite-on-full — newHeads subscribers care about the latest head, not a backlog, so stale heads are dropped in favor of new ones.

Design notes

A simpler alternative was to keep using the event bus and just have giga_router call eventBus.PublishEventNewBlockHeader(...) after commit. That would have been a ~20-line diff with zero evmrpc changes (the existing consumer would just work). We opted not to because (1) the pubsub layer isn't doing anything load-bearing for this use case — one fixed query, one subscriber — and (2) we plan to drop the cosmos event bus entirely once external consumers move off tm.event='Tx' and friends. A direct channel is the end-state design; this PR lands it now to avoid carrying the stopgap.

hash on the published header is the autobahn lane-block header hash (the same value the EVM receipt store records as blockHash), not a hash over the synthesized Tendermint header. parentHash / receiptsRoot / transactionsRoot are zero under Autobahn — the lane-block path doesn't compute a Tendermint-style hash chain, and surfacing fakes would be worse than zeros. Subscribers that chain-validate the head stream will need a different mechanism going forward; this is called out in the encoder's doc.

Test plan

  • gofmt -s -l clean on all modified files; go build ./... clean.
  • go test ./evmrpc/ — 249 PASS, 8 SKIP, 0 FAIL (22s). Includes:
    • existing TestSubscribeNewHeads (legacy event-bus path) — unchanged behaviour.
    • new TestSubscribeNewHeadsAutobahn (notifier path, in-process WS server end-to-end).
    • new unit tests for BlockHeaderNotifier (deliver, overwrite-on-full, nil-safe) and encodeCommittedBlock (happy path + nil ConsensusParamUpdates).
  • evm_rpc_tests.sh (HTTP-method fixtures) against a real non-Autobahn cluster — 160/160 pass, no regressions on the request/response RPC surface.
  • New integration_test/evm_module/ws_test/ — consensus-mode-agnostic, gated on SEI_EVM_WS_RUN_INTEGRATION=1, wired into evm_rpc_tests.sh. Run end-to-end against both cluster modes:
    • Non-Autobahn cluster: PASS (number=0x2b, hash 0xe0dd089b...)
    • Autobahn cluster: PASS (number=0xf5, hash 0xc4d1bc59..., GigaRouter initialized confirmed in all 4 node logs)
  • Reviewer: confirm non-app-hash-breaking label is correct — this PR only adds a notification side-effect after commit; no state machine or app-hash-relevant logic changes.

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new post-commit callback on Autobahn’s block execution hot path and changes eth_subscribe("newHeads") delivery to use an in-process channel when enabled; mistakes could drop notifications or affect performance, though legacy CometBFT behavior is preserved.

Overview
Fixes eth_subscribe("newHeads") under Autobahn by bypassing the Tendermint event bus and instead pushing committed block headers through a new in-process BlockHeaderNotifier.

giga_router now invokes an optional types.BlockHeaderListener after successful Commit, the app wires this to the notifier only when Autobahn is enabled, and evmrpc.SubscriptionAPI switches between the legacy event-bus subscription and the notifier-fed fanout. The Autobahn encoder uses the Autobahn-provided block hash, derives gasUsed from tx results, reads gasLimit from current consensus params, and fixes baseFeePerGas selection to use the parent height.

Adds unit + WS end-to-end tests for notifier semantics (overwrite-on-full) and Autobahn newHeads delivery, plus a gated integration WS test and script updates to run it.

Reviewed by Cursor Bugbot for commit 1cd5600. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMay 12, 2026, 7:57 PM

@wen-coding wen-coding changed the title Wire eth_subscribe(newHeads) for Autobahn via in-process notifier Wire eth_subscribe(newHeads) for Autobahn (CON-257) May 11, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 71.66667% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.35%. Comparing base (26636ba) to head (1cd5600).

Files with missing lines Patch % Lines
evmrpc/subscribe.go 81.01% 9 Missing and 6 partials ⚠️
sei-tendermint/internal/p2p/giga_router.go 0.00% 8 Missing and 1 partial ⚠️
app/app.go 0.00% 6 Missing and 1 partial ⚠️
evmrpc/notifier.go 88.88% 2 Missing ⚠️
sei-tendermint/node/setup.go 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3419      +/-   ##
==========================================
+ Coverage   59.34%   59.35%   +0.01%     
==========================================
  Files        2112     2113       +1     
  Lines      174772   174863      +91     
==========================================
+ Hits       103724   103796      +72     
- Misses      62010    62027      +17     
- Partials     9038     9040       +2     
Flag Coverage Δ
sei-chain-pr 68.42% <71.66%> (?)
sei-db 70.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
evmrpc/server.go 87.79% <100.00%> (ø)
sei-tendermint/node/node.go 65.28% <100.00%> (+0.09%) ⬆️
sei-tendermint/node/public.go 67.44% <100.00%> (+3.33%) ⬆️
sei-tendermint/node/seed.go 48.48% <ø> (ø)
sei-tendermint/node/setup.go 69.35% <0.00%> (-0.23%) ⬇️
evmrpc/notifier.go 88.88% <88.88%> (ø)
app/app.go 68.93% <0.00%> (-0.26%) ⬇️
sei-tendermint/internal/p2p/giga_router.go 66.02% <0.00%> (-2.98%) ⬇️
evmrpc/subscribe.go 68.69% <81.01%> (+6.84%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread evmrpc/subscribe.go
wen-coding added a commit that referenced this pull request May 11, 2026
encodeCommittedBlock was mirroring the legacy encodeTmHeader, which
emits both the typo'd "withdrawlsRoot" and the correctly-spelled
"withdrawalsRoot". Since the Autobahn encoder is new code there is no
back-compat reason to carry the typo; legacy encodeTmHeader is left
untouched.

Reported by Cursor Bugbot on #3419.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread evmrpc/subscribe.go
@wen-coding wen-coding requested a review from amir-deris May 12, 2026 13:53
wen-coding added a commit that referenced this pull request May 12, 2026
ResponseFinalizeBlock.ConsensusParamUpdates is only populated when the
app proposes a consensus-param update — nil on the vast majority of
blocks. Reading it for newHeads gasLimit means the reported gasLimit
would be 0 for nearly every notification under Autobahn. Match the
pattern already used in evmrpc/block.go's GetBlockByNumber: pull
gasLimit from ctx.ConsensusParams() in the consumer goroutine and pass
it as a parameter into encodeCommittedBlock.

Reported by Cursor Bugbot on #3419 (high severity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 84cd2e6. Configure here.

Comment thread evmrpc/subscribe.go
wen-coding and others added 6 commits May 12, 2026 10:02
Under Autobahn, app.FinalizeBlock is driven by giga_router rather than
CometBFT consensus, so the legacy Tendermint event-bus subscription
that backs eth_subscribe("newHeads") never fires. This adds a direct
in-process notifier from giga_router's commit path into evmrpc's
SubscriptionAPI fan-out (overwrite-on-full, capacity 1, so the latest
head always wins if a consumer lags). The legacy path is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
encodeCommittedBlock was mirroring the legacy encodeTmHeader, which
emits both the typo'd "withdrawlsRoot" and the correctly-spelled
"withdrawalsRoot". Since the Autobahn encoder is new code there is no
back-compat reason to carry the typo; legacy encodeTmHeader is left
untouched.

Reported by Cursor Bugbot on #3419.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tighten hash field doc in blockHeaderEvent: receipt store records
  zero blockHash on disk and evmrpc overlays the autobahn hash at read
  time. The prior wording implied the receipt store stored it directly.
- Clarify single-producer assumption in OnBlockCommitted and document
  multi-producer behaviour (still safe, latest-wins is acceptable).
- Move SubscriptionManager construction inside the legacy event-bus
  branch in NewSubscriptionAPI so the field is nil under Autobahn
  instead of dead state.
- Document the non-nil header/response contract on BlockHeaderListener,
  and add a defensive guard in runNewHeadsFromNotifier so a single
  malformed event does not kill the fan-out goroutine for all
  subscribers.
- Annotate the listener call site in giga_router.executeBlock so the
  fire-after-Commit-before-mempool-update ordering is explicit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ResponseFinalizeBlock.ConsensusParamUpdates is only populated when the
app proposes a consensus-param update — nil on the vast majority of
blocks. Reading it for newHeads gasLimit means the reported gasLimit
would be 0 for nearly every notification under Autobahn. Match the
pattern already used in evmrpc/block.go's GetBlockByNumber: pull
gasLimit from ctx.ConsensusParams() in the consumer goroutine and pass
it as a parameter into encodeCommittedBlock.

Reported by Cursor Bugbot on #3419 (high severity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GetNextBaseFeePerGas(ctx_at_N) returns the base fee for block N+1, not
block N. The previous code passed the current block's ctx, so the
newHeads notification for block N reported what should have been
attached to N+1.

Match the pattern from block.go's GetBlockByNumber: derive baseFee
from the parent block's ctx, with a genesis (height==1) fallback to
DefaultMinFeePerGas. The legacy CometBFT path has the same bug but is
left alone — the more important consistency to restore is between
eth_subscribe(newHeads) and eth_getBlockByNumber on the same Autobahn
cluster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ExecTxResult has both a GasWanted and a GasUsed field with different
semantics. The variable was accumulating GasUsed but named "gasWanted",
inviting a future field-swap regression. Rename for clarity. Legacy
encodeTmHeader carries the same anti-pattern but is left alone.

Reported by Cursor Bugbot on #3419 (low severity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wen-coding wen-coding force-pushed the wen/add_eth_subscribe_newhead_for_autobahn branch from 073496b to c8c0d46 Compare May 12, 2026 17:04

// First message must be the subscription confirmation. Bound the wait
// so a broken handshake fails fast.
if err := conn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This err shadows definition of err on line 42. Can just reuse it (similarly in other lines):
if err = conn.SetReadDeadline....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

wen-coding and others added 2 commits May 12, 2026 10:37
The five `if err := ...` blocks in TestEthSubscribeNewHeads each
shadowed the outer err from the initial Dial call. Reuse with `err =`
for clarity (the outer err is already in scope and gets overwritten
on each call anyway).

Reported by @amir-deris on #3419.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The base-fee selection in runNewHeadsFromNotifier was the actual
location of the off-by-one fix (parent ctx vs current ctx) but had no
direct test coverage — encodeCommittedBlock takes baseFee as an arg, so
testing it only proved the encoder forwards the value it's given, not
that the caller picks the right ctx.

Extract pickHeadBaseFee as a free function that takes getNextBaseFee
as a function pointer (rather than reaching into *keeper.Keeper), then
spy on the ctxProvider in tests to verify:
- TestPickHeadBaseFee_UsesParentCtx: ctxProvider called with height-1
  (NOT height), and result is forwarded.
- TestPickHeadBaseFee_GenesisFallback: at height 1 the keeper call is
  skipped entirely and DefaultMinFeePerGas is returned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants